Skip to content

Conversation

rekmarks
Copy link
Member

@rekmarks rekmarks commented Jul 23, 2025

Explanation

Introduces JsonRpcEngineV2 and JsonRpcServer, intended to replace all existing usage of the existing JsonRpcEngine implementation. For the motivation behind this change, see #6088 and/or this ADR.

Implementation

In order to resolve the problems listed in the motivation, V2 engine is split in two:

  • JsonRpcEngineV2
    • Orchestrates middleware and composes sub-engines
    • JSON-RPC requests go in, result values (not JSON-RPC responses) come out
    • Re-throws middleware errors directly
  • JsonRpcServer
    • JSON-RPC requests go in, JSON-RPC responses come out
    • Accepts an errorHandler constructor parameter for capturing engine / middleware errors

See the updated package README.md for details.

Migration

While this PR is substantial, migrating our existing JSON-RPC pipelines will be a significant project involving multiple teams over many releases cycles. To facilitate this, the PR introduces a forwards-compatibility adapter, asV2Middleware(), and backwards-compatibility adapter, asLegacyMiddleware(), for the legacy and V2 engines, respectively. In addition, all V2 exports are exposed under the /v2 export path, making this update completely non-breaking (although all legacy exports are deprecated).

Note to reviewers

I recommend proceeding as follows:

  1. Read the readme
  2. JsonRpcEngineV2
  3. JsonRpcServer
  4. Compatibility adapter functions

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
    • Migrating our RPC pipelines left as an exercise to the reader.

Note

Introduces JsonRpcEngineV2 and JsonRpcServer, adds v2/ exports and adapters to interoperate with the legacy engine, updates docs, tests, and package metadata.

  • json-rpc-engine (v2):
    • Add JsonRpcEngineV2 core with immutable request/result handling, MiddlewareContext, and utilities (isRequest, isNotification, JsonRpcEngineError, stringify).
    • Add JsonRpcServer wrapper for JSON-RPC-compliant request/response handling and error serialization.
    • Add compatibility adapters: v2/asLegacyMiddleware and legacy asV2Middleware to bridge between engines; include propagation utilities.
  • Legacy engine:
    • Mark legacy JsonRpcEngine and helpers as deprecated; minor internal refactors to use shared stringify and annotations.
    • Add legacy-focused README under src/README.md.
  • Packaging/exports:
    • Expose v2 via @metamask/json-rpc-engine/v2 (ESM/CJS) and add v2.js for Browserify compatibility.
    • Update package.json exports and add deps (deep-freeze-strict, klona, @types/deep-freeze-strict).
  • Docs/Changelog:
    • Revamp root README to emphasize v2 usage; update changelog with Added/Deprecated notes.
  • Tests:
    • Comprehensive new test suites for v2 engine, server, adapters, and utilities; adjust existing tests accordingly.

Written by Cursor Bugbot for commit 7d7ded1. This will update automatically on new commits. Configure here.

@rekmarks rekmarks force-pushed the rekm/json-rpc-engine-rewrite-next branch from f74dff2 to c393838 Compare July 23, 2025 18:10
rekmarks added 29 commits July 23, 2025 16:29
cursor[bot]

This comment was marked as resolved.

Copy link
Member

@FrederikBolding FrederikBolding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test to ensure that parallel processing of RPC requests work?

Copy link
Member Author

@rekmarks rekmarks Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a8d4eb6

edit: demonstrates that multiple requests can be in flight simultaneously and that the context is isolated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the processing was sequential the added test would still pass I think. Do we not at a minimum need an element of time difference between the requests? Ideally we can prove that requests don't always flow through in the order they are dispatched in

Copy link
Member Author

@rekmarks rekmarks Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

866aed6 demonstrates that request are not processed serially

@rekmarks rekmarks enabled auto-merge (squash) October 13, 2025 18:21
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had one more comment about typing context, but I have finally gone through everything in this PR so I believe that is the only thing that's left.

return next();
});
const middleware2 = jest.fn(({ context }) => {
return context.get('foo') as string | undefined;
Copy link
Contributor

@mcmire mcmire Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to avoid from needing to typecast context values? I see that we have assertGet, so I imagine that this would be the primary way to access values in the context instead of get, but since we aren't performing any validation on values it seems like a fancy typecast.

If the request and result are generic, should the context be generic too? Perhaps we could add another type parameter to JsonRpcMiddleware, after the request. Maybe it would work something like this:

const middleware1 = (({ context, next }) => {
  return context.get('foo') < 30 ? 'bar' : next();
}) satisfies JsonRpcMiddleware<
  JsonRpcRequest,
  // The second parameter is now the content of the context
  { foo: number },
  'bar' | true
>;

const middleware2 = (() => {
  return true;
}) satisfies JsonRpcMiddleware<JsonRpcRequest, {}, true>;

const engine = new JsonRpcEngineV2<
  JsonRpcRequest,
  // The second parameter is now the content of the context
  { foo: number },
  'bar' | true
>({
  middleware: [middleware1, middleware2],
});

const request = makeRequest();

const result1 = await engine.handle(request, {
  context: new MiddlewareContext({ foo: 31 }),
});  //=> 'bar'

const result2 = await engine.handle(request, {
  context: new MiddlewareContext({ foo: 29 }),
});  //=> true

To do this, I suppose MiddlewareContext would have to be a wrapper around an object rather than a subclass of Map.

Would this introduce safety issues and/or screw up compatibility between V1 and V2?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have a solution for this, will give it a go.

};

export type JsonRpcMiddleware<
Request extends JsonRpcCall = JsonRpcCall,
Copy link
Contributor

@mcmire mcmire Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Thought] In providing default values for these parameters are we expecting engineers to be using this type directly? If so, I wonder if that is the most ergonomic approach. I wonder if we can almost always have TypeScript infer this. Maybe not something we have to improve now, but just a thought.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inference at the time of engine construction generally works, although if you're defining a middleware function in one place for an engine that lives elsewhere, you probably want to use this type to ensure that your middleware function is valid. Do you have any ideas as to how we would make that more ergonomic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be curious how far we could get with removing the default values. I was mainly thinking it would reduce the size of this file. But maybe it wouldn't, and perhaps you're right that in practice it wouldn't matter as developers would need to pass in these types anyway. Like I said, just a thought as I worked with this file a bit locally, not anything concrete to suggest here.


describe('requests', () => {
it('returns a result from the middleware', async () => {
const middleware: JsonRpcMiddleware<JsonRpcRequest> = jest.fn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Thought] I wonder if we should do another pass through these tests at some point and convert these type assertions into satisfies? I do agree with Jongsun that in general we should be promoting use of satisfies at least for non-final types instead of type assertions, so that we can have TypeScript infer as much as it can. I'd at least want the tests to serve as examples and represent best practices for how authors should define middleware.

Using a generic Result forced each middleware function to have the same
return type. This makes the generic useless. Supporting this, almost all
of our uses of the equivalent generic on the legacy JsonRpcMiddlware
type are Json, any, or unknown. In consequence, we remove the Result
generic.

In addition, unfreezes the result returned from asLegacyMiddleware, a
problem that was surfaced by the type refactor. Adds a test to verify
this behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[json-rpc-engine] Rewrite JsonRpcEngine for safety, ergonomics, and debuggability

5 participants